/**
 * 
 */
package IC.Analyzer;

import java.util.ArrayList;
import java.util.List;

import IC.AST.*;
import IC.Parser.sym;
import IC.Analyzer.Types.*;
import IC.AST.nodes.*;
import IC.Analyzer.SymTabEntry.*;


/**
 * Build the symtab recursively, top->down:
 * 1. populate symtab entries with: id, type, kind, declaration node
 * 2. each method is called with the symtab of its parent, e.g context
 * 3. the up-type is used rarely for propogating declaration types to
 *    put them in the symtab
 *
 * Check:
 * 	1. class hierarchy is a tree
 *  2. all variables are declared before used inside a block
 *  3. all types are well-defined
 *  4. methods, classes, local vars, fields, formals, are not redefined
 *  	in the same scopes
 *  5. fields are not overriden in subclasses
 * 
 * Do:
 *  1. build the type objects from declarations and put in symtab
 *  2. connect symtab to each and every node
 *  3. generate 'this' and 'return' special entries
 *  4. add the type property to each node that declares a type
 *  
 */
public class SymTabBuilder extends Checker<SymTable,Object>
implements PropogatingVisitor<SymTable,Object> {

	public SymTabBuilder(Program prog) throws Exception {
		recoverErrors();
		visit(prog,null);
		if (hadError()) {
			throw new SemanticError(prog,"program is incorrect");
		}
	}

	/**
	 * 1. process all class declarations
	 * 2. build the class hirarchy:
	 * 		2.1. a static symtab and dynamic symtab for each classdecl
	 * 		2.2. conect to super-class symtab
	 * 		2.3. visit dymamic methods and static methods 
	 */
	public Object visit(Program that, SymTable context) throws Exception {
		SymTable symtab = new SymTable(context); 
		that.setSymTab(symtab);

		// first add all classes to the context as types
		for (ClassDecl decl : that.getClasses()) {
			String id = decl.getClassID();

			if (symtab.resolve(id) != null)
				error(decl,"redefinition of class `"+id+"'");
			else {
				ClassType baseType = null; 
				SymTable staticScope = context, virtualScope = context;

				if (decl.getBaseClassID() != null) {
					// resolve base class type
					String baseClassID = decl.getBaseClassID();
					SymTabEntry baseEntry = symtab.resolve(baseClassID);
					if (baseEntry == null || !baseEntry.isClass()) {
						error(decl,"undefined base class `"+baseClassID+"'");
						return null; // continue?
					}
					else {
						// find base class scopes
						ClassDecl baseClassDecl = (ClassDecl)baseEntry.getDeclNode();
						baseType = (ClassType)baseEntry.getType();
						virtualScope = new SymTable(baseClassDecl.getSymTab());
						staticScope = new SymTable(baseClassDecl.getStaticSymTab());
					}
				} 
				else {
					virtualScope = new SymTable(symtab);
					staticScope = new SymTable(symtab);
				}

				// store static and dynamic scopes
				decl.setSymTab(virtualScope);
				decl.setStaticSymTab(staticScope);

				// add "this" to the dynamic scope
				ClassType classT = ClassType.getIt(decl, baseType);
				virtualScope.add("this", new SymTabEntry(decl, classT, IdKind.THIS));

				// add the class scope
				symtab.add(id, new SymTabEntry(decl,classT));
			}
		}

		// scan the classes
		for (ClassDecl decl : that.getClasses()) 
		{
			// visit the class declaration
			decl.accept(this,symtab);
		}
		return null;
	}

	/**
	 * visit a class declaration - 
	 * 1. add myself to the context
	 * 2. make static scope and dynamic scope which inherit from 
	 * 		respective base class scopes
	 * 3. visit the fields (so methods will see the vars)
	 * 4. visit the methods. add static to static scope,
	 * 		and dynamic to both scopes
	 */
	public Object visit(ClassDecl that, SymTable context) throws Exception {
		// get both class scopes
		SymTable virtualScope = that.getSymTab();
		SymTable staticScope = that.getStaticSymTab();

		// treat fields and methods seperately
		List<MethodDecl> methods = new ArrayList<MethodDecl>();

		// visit fields and cache methods
		for (ASTNode decl: that.getClassMembers()) {
			if (decl instanceof MethodDecl) {
				methods.add((MethodDecl)decl);
			} else {
				decl.accept(this, virtualScope);
			}
		}

		// visit methods
		for (MethodDecl decl: methods) {
			String methodID = decl.getMethodName();
			SymTable scope 		= decl.isStatic()?staticScope:virtualScope; 
			SymTable otherScope = !decl.isStatic()?staticScope:virtualScope; 


			/*
			 * verify that if a method is redefined, then:
			 * 1. it overrides a method
			 * 2. the overriden method is in a superclass
			 * 3. no cross-storage-class overrides
			 * 
			 */

			if (scope != null && scope.isDefinedHere(methodID)) {
				// make sure it is not defined here
				error(decl,"redefinition of `"+methodID+"'");
			}
			else if (otherScope != null && otherScope.isDefined(methodID)) {
				// make sure it is not defined in other storage class 
				error(decl,"invalid storage class in redefinition of `"+methodID+"'");
			}
			else if(scope != null) {
				// verify signatures in the method processor
				decl.accept(this, scope);
			}
		}
		return null;
	}

	/**
	 * visit class fields -
	 * 1. visit the type
	 * 2. add the ID's
	 */
	public Object visit(FieldsDecl that, SymTable context) throws Exception {

		// visit the type declaration to get the type object,
		// or an exception
		that.setSymTab(context);
		Type t = (Type)that.getType().accept(this,context);

		// add the fields to the class symtab
		for (String id : that.getIDs()) {
			if (context.isDefined(id))
				error(that,"redefinition of `"+id+"'");
			else
				context.add(id, new SymTabEntry(that, t ,IdKind.MEMBERVAR));
		}
		return null;
	}

	/**
	 * visit method - 
	 * 1. visit the type
	 * 2. add method to context
	 * 3. make method symtab
	 * 4. visit formals list
	 * 5. visit the block
	 * @return the symbol table entry of the method
	 */
	public Object visit(MethodDecl that, SymTable context) throws Exception {
		SymTable symtab = new SymTable(context);
		that.setSymTab(symtab);

		// process all formals declarations and
		// collect their types
		List<Type> argTypes = new ArrayList<Type>();
		for (FormalDecl fd: that.getFormals()) {
			argTypes.add((Type)fd.accept(this,symtab));
		}

		// gret return type
		Type retType = (that.getReturnType()!=null)?
				(Type)that.getReturnType().accept(this,symtab):VoidType.getIt();


				/* 
				 * check method overriding
				 * add the method to the context only if it didnt 
				 * exist before, or if it overrides a method with the sam signature
				 * 
				 */
				MethodType myType = MethodType.getIt(retType, argTypes);
				String id = that.getMethodName();
				SymTabEntry e = context.resolve(id);
				if (e != null && ! e.getType().equals(myType))
					error(that,"type mismatch while overriding `"+id+"'");
				else {
					context.add(that.getMethodName(), new SymTabEntry(that,myType));

					// add return type to the method context
					symtab.add("return", 
							new SymTabEntry(that.getReturnType(), retType, IdKind.RETURNTYPE));		

					// visit the body of just return null
					if (that.getBody() != null)
						that.getBody().accept(this,symtab);
				}
				return null;
	}

	/**
	 * visit a primitive type declaration
	 * @return the type object
	 */
	public Object visit(PrimitiveTypeDecl that, SymTable context) throws Exception {
		that.setSymTab(context);
		switch (that.getKind()) {
		case sym.INT:
			that.setType(IntType.getIt()); break;
		case sym.STRING:
			that.setType(StringType.getIt()); break;
		case sym.BOOLEAN:
			that.setType(BoolType.getIt()); break;
		default:
			return null;
		}
		return that.getType();
	}

	/**
	 * visit an array type
	 * @return the type object
	 */
	public Object visit(ArrayTypeDecl that, SymTable context) throws Exception {
		that.setSymTab(context);
		Type elmType = (Type)that.getElemType().accept(this,context);
		that.setType(ArrayType.getIt(elmType));
		return that.getType();
	}

	/**
	 * visit a class type declaration
	 * @return the resulting type
	 */
	public Object visit(ClassTypeDecl that, SymTable context) throws Exception {
		that.setSymTab(context);
		SymTabEntry e = context.resolve(that.getClassID());
		if (e != null && e.isClass()) {
			that.setType(e.getType());
			return that.getType();
		} else {
			error(that, "the class `"+that.getClassID()+"' is undefined");
			return null;
		}
	}

	/**
	 * visit formal decl - add to the function
	 * @return the formal type
	 */
	public Object visit(FormalDecl that, SymTable context) throws Exception {
		that.setSymTab(context);
		Type t = (Type)that.getType().accept(this,context);
		String id = that.getID();
		if (context.isDefinedHere(id))
			error(that,"redefinition of `"+id+"'");
		else
			context.add(id, new SymTabEntry(that,t,IdKind.FORMALPARAM));
		return t;
	}

	/**
	 * visit a variable
	 * 1. verify that it has been declared
	 */
	public Object visit(SimpleVar that, SymTable context) throws Exception {
		that.setSymTab(context);
		SymTabEntry e = context.resolve(that.getID());
		if (e == null || !e.isVar())
			error(that,"undefined variable `"+that.getID()+"'");
		return null;
	}

	/**
	 * visit 'this'
	 * 1. check that it is defined in current scope
	 */
	public Object visit(ThisExpr that, SymTable context) throws Exception {
		that.setSymTab(context);
		SymTabEntry e = context.resolve("this");
		if (e == null || !e.isThis())
			error(that,"`this' is not accessible");
		return null;
	}

	/**
	 * visit statement block - 
	 * 1. make a new symtab
	 * 2. visit all statements
	 */
	public Object visit(StmtBlock that, SymTable context) throws Exception {
		SymTable symtab = new SymTable(context);
		that.setSymTab(symtab);
		for (Statement s: that.getStmts()) {
			s.accept(this, symtab);
		}
		return null;
	}

	/**
	 * visit a variable declaration statement
	 * 1. add the variable to the symbol table
	 */
	public Object visit(VarDeclStmt that, SymTable context) throws Exception {

		// this is done to prevent this:
		// 		while(b) int x;
		// from registering x in the method scope
		that.setSymTab(context);
		Type t = (Type)that.getType().accept(this, context);

		// analyze init expression
		if (that.getInit() != null)
			that.getInit().accept(this, context);

		// add the local var
		String id = that.getVar().getID();
		if (context.isDefinedHere(id)) {
			error(that,"redefinition of `"+id+"'");
		}
		// 	to catch this:
		//  void func(int x) {
		//   int x;
		//  }
		else {
			SymTable parent = context.getParent();
			if ((parent != null) && parent.isDefinedHere(id)
						&& parent.resolve(id).isFormalParam()) {
				error(that,"shadowing of formal parameter with local variable `"+id+"'");
			}
			// everything is Ok
			else {
				context.add(id, new SymTabEntry(that, t, IdKind.LOCALVAR));
			}
		}
		
		// visit the declared variable
		that.getVar().accept(this, context);
		return null;
	}

	/**
	 * visit a While statement - make new symtab for the body
	 */
	public Object visit(WhileStmt that, SymTable context) throws Exception {
		SymTable symtab = new SymTable(context);
		that.setSymTab(symtab);
		that.getCondition().accept(this, symtab);
		that.getBody().accept(this, symtab);
		return null;
	}

	/**
	 * visit an If statement - make new symtab for then+else
	 */
	public Object visit(IfStmt that, SymTable context) throws Exception {
		SymTable symtab = new SymTable(context);
		that.setSymTab(symtab);
		that.getCondition().accept(this, symtab);
		that.getIfStmt().accept(this, symtab);
		if (that.getElseStmt() != null) {
			that.getElseStmt().accept(this, symtab);
		}
		return null;
	}

	/** 
	 * visit functions for trivial cases 
	 */
	public void processPre(ASTNode node, SymTable context) {
		node.setSymTab(context);
	}

}
